{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Face Recognition using Support Vector Classifier\n",
    "\n",
    "    PCA and LDA are used to reduce dimensionality. This is followed by randomized search for optimizing the hyperparameters of SVC. \n",
    "    \n",
    "    Results: Precision  Recall  F1\n",
    "    With PCA  0.94      0.91      0.92\n",
    "    With LDA  0.98      0.96      0.96"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##  Import packages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "import numpy as np\n",
    "import cv2\n",
    "import matplotlib.pyplot as plt\n",
    "from time import time\n",
    "import os"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##  Prepare data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Each image is resized to (64, 64)\n"
     ]
    }
   ],
   "source": [
    "# Handle resizing of images, while maintaing aspect ratio\n",
    "def resize_image(image, h, w):\n",
    "\n",
    "    old_width = image.shape[1]\n",
    "    old_height = image.shape[0]\n",
    "    dH, dW = 0, 0\n",
    "    \n",
    "    if old_width < old_height:  #width is smaller, resize along width and compute new height\n",
    "        aspect_r = w / old_width  #aspect ratio\n",
    "        new_height = int(old_height * aspect_r)\n",
    "        new_dim = (w, new_height)\n",
    "        resized_img = cv2.resize(image, new_dim, interpolation = cv2.INTER_AREA)\n",
    "        #dimensions of above image are: (new_height, desired_width). \n",
    "        #Since images in our dataset might be of diff. sizes, such resized images will end up being different sizes\n",
    "        #hence crop the image as follows to make each image of size(desired_height, desired_width)\n",
    "        dH = int((resized_img.shape[0] - h)/2.0)\n",
    "        \n",
    "    else:   #height is smaller, resize along height and compute new width\n",
    "        aspect_r = h / old_height  #aspect ratio\n",
    "        new_width = int(old_width * aspect_r)\n",
    "        new_dim = (new_width, h)\n",
    "        resized_img = cv2.resize(image, new_dim, interpolation = cv2.INTER_AREA)\n",
    "        dW = int((resized_img.shape[1] - w)/2.0)\n",
    "        \n",
    "    new_h, new_w = resized_img.shape[:2]\n",
    "    cropped_image = resized_img[dH:new_h - dH, dW:new_w - dW]  #dimensions of this image = desired dimensions\n",
    "    #Make sure that the image is resized to desired dimensions\n",
    "    return cv2.resize(cropped_image, (h, w), interpolation = cv2.INTER_AREA)  \n",
    "\n",
    "#All images provided to the classifier should have fixed and equal sizes\n",
    "dim = (64, 64)\n",
    "\n",
    "print(\"Each image is resized to {}\".format(dim))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Subject 0, 10 images loaded\n",
      "Subject 1, 10 images loaded\n",
      "Subject 2, 10 images loaded\n",
      "Subject 3, 10 images loaded\n",
      "Subject 4, 10 images loaded\n",
      "Subject 5, 10 images loaded\n",
      "Subject 6, 10 images loaded\n",
      "Subject 7, 10 images loaded\n",
      "Subject 8, 10 images loaded\n",
      "Subject 9, 10 images loaded\n",
      "Subject 10, 10 images loaded\n",
      "Subject 11, 10 images loaded\n",
      "Subject 12, 10 images loaded\n",
      "Subject 13, 10 images loaded\n",
      "Subject 14, 10 images loaded\n",
      "Subject 15, 10 images loaded\n",
      "Subject 16, 10 images loaded\n",
      "Subject 17, 10 images loaded\n",
      "Subject 18, 10 images loaded\n",
      "Subject 19, 10 images loaded\n",
      "Subject 20, 10 images loaded\n",
      "Subject 21, 10 images loaded\n",
      "Subject 22, 10 images loaded\n",
      "Subject 23, 10 images loaded\n",
      "Subject 24, 10 images loaded\n",
      "Subject 25, 10 images loaded\n",
      "Subject 26, 10 images loaded\n",
      "Subject 27, 10 images loaded\n",
      "Subject 28, 10 images loaded\n",
      "Subject 29, 10 images loaded\n",
      "Subject 30, 10 images loaded\n",
      "Subject 31, 10 images loaded\n",
      "Subject 32, 10 images loaded\n",
      "Subject 33, 10 images loaded\n",
      "Subject 34, 10 images loaded\n",
      "Subject 35, 10 images loaded\n",
      "Subject 36, 10 images loaded\n",
      "Subject 37, 10 images loaded\n",
      "Subject 38, 10 images loaded\n",
      "Subject 39, 10 images loaded\n"
     ]
    }
   ],
   "source": [
    "def fetch_subject_images(path_to_subject, subject_number):\n",
    "    X = np.array([])\n",
    "    index = 0\n",
    "    for subject_img in os.listdir(path_to_subject): #for each image in this subject's folder\n",
    "        img_path = os.path.join(path_to_subject, subject_img)\n",
    "        if img_path.endswith(\".pgm\") or img_path.endswith(\".png\") or img_path.endswith(\".jpg\") or img_path.endswith(\".jpeg\"):\n",
    "            #Read image, convert it to grayscale and resize every image to a fixed size  \n",
    "            img = cv2.imread(img_path, 0)\n",
    "            img = resize_image(img, dim[0], dim[1]) \n",
    "            img_data = img.ravel()  #Flatten each image, so that each sample is a 1D vector\n",
    "            X = img_data if not X.shape[0] else np.vstack((X, img_data))\n",
    "            index += 1\n",
    "\n",
    "    y = np.empty(index, dtype = int) #index = total no. of samples\n",
    "    y.fill(subject_number)  #add labels\n",
    "    return X, y\n",
    "\n",
    "def fetch_data(dataset_path):\n",
    "\n",
    "    # Get a the list of folder names in the path to dataset\n",
    "    labels_list = [d for d in os.listdir(dataset_path) if \".\" not in str(d)]\n",
    "\n",
    "    X = np.empty([0, dim[0]*dim[1]])\n",
    "    y = np.empty([0])\n",
    "\n",
    "    for i in range(0, len(labels_list)):  #for each person\n",
    "        subject = str(labels_list[i])  #person i in list of ppl\n",
    "        path_to_subject = os.path.join(dataset_path, subject) #full path to this person's directory\n",
    "        \n",
    "        #Read all images in this folder (all images of this person)\n",
    "        X_, y_ = fetch_subject_images(path_to_subject, i)\n",
    "        X = np.concatenate((X, X_), axis=0)\n",
    "        y = np.append(y, y_)\n",
    "        print(\"Subject {}, {} images loaded\".format(i, X_.shape[0]))\n",
    "\n",
    "    return X, y, labels_list\n",
    "\n",
    "# Load training data \n",
    "dataset_path = \"att_faces/\"\n",
    "X, y, labels_list_faces  = fetch_data(dataset_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Split into training and test sets"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Dataset consists of 400 samples and  40 classes\n",
      "Data: (400, 4096) and labels: (400,)\n",
      " \n",
      "Training data (320, 4096), test data (80, 4096)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "n_classes = len(np.unique(y))\n",
    "print(\"Dataset consists of {} samples and  {} classes\".format(X.shape[0], n_classes))\n",
    "print(\"Data: {} and labels: {}\".format(X.shape, y.shape))                                                                                           \n",
    "print(\" \")\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)\n",
    "print(\"Training data {}, test data {}\".format(X_train.shape, X_test.shape))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#  Principal Component Analysis (PCA)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PCA reduced dimensionality from 4096 to 118\n",
      "New training and test data (320, 118) and (80, 118)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.decomposition import PCA\n",
    "\n",
    "pca = PCA(n_components = 0.95, whiten=True).fit(X_train)  #preserve 95% of variance\n",
    "print(\"PCA reduced dimensionality from {} to {}\".format(X_train.shape[1], pca.n_components_))\n",
    "\n",
    "X_train_pca = pca.transform(X_train)\n",
    "X_test_pca = pca.transform(X_test)\n",
    "\n",
    "print(\"New training and test data\", X_train_pca.shape, \"and\", X_test_pca.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fitting 3 folds for each of 10 candidates, totalling 30 fits\n",
      "Randomized search took 0.03059083620707194 minutes\n",
      "Best estimator:\n",
      "SVC(C=10.265475491159425, cache_size=7000, class_weight=None, coef0=0.0,\n",
      "  decision_function_shape='ovr', degree=3, gamma=0.0035805796748390236,\n",
      "  kernel='rbf', max_iter=-1, probability=False, random_state=None,\n",
      "  shrinking=True, tol=0.001, verbose=False)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[Parallel(n_jobs=1)]: Done  30 out of  30 | elapsed:    1.8s finished\n"
     ]
    }
   ],
   "source": [
    "from time import time\n",
    "from sklearn.svm import SVC\n",
    "from sklearn.model_selection import RandomizedSearchCV\n",
    "from scipy.stats import reciprocal, uniform\n",
    "\n",
    "svm_clf1 = SVC(kernel = \"rbf\", cache_size = 7000)\n",
    "param_grid = {\"gamma\": reciprocal(0.001, 0.1), \"C\": uniform(1, 10)}\n",
    "rand_search_cv = RandomizedSearchCV(svm_clf1, param_grid, n_iter = 10, verbose = 1)\n",
    "#the search is cross-validated (defult 3 fold)\n",
    "\n",
    "start = time()\n",
    "rand_search_cv.fit(X_train_pca, y_train)\n",
    "print(\"Randomized search took {} minutes\".format((time() - start)/60.0))\n",
    "\n",
    "print(\"Best estimator:\")\n",
    "print(rand_search_cv.best_estimator_)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Classification report"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Classifier's accuracy: 0.9125\n",
      "             precision    recall  f1-score   support\n",
      "\n",
      "        0.0       0.75      1.00      0.86         3\n",
      "        1.0       1.00      1.00      1.00         1\n",
      "        2.0       1.00      1.00      1.00         2\n",
      "        3.0       1.00      1.00      1.00         4\n",
      "        4.0       1.00      1.00      1.00         3\n",
      "        5.0       1.00      1.00      1.00         3\n",
      "        7.0       1.00      0.67      0.80         6\n",
      "        8.0       1.00      1.00      1.00         2\n",
      "        9.0       1.00      1.00      1.00         2\n",
      "       10.0       1.00      1.00      1.00         2\n",
      "       11.0       1.00      1.00      1.00         3\n",
      "       12.0       0.00      0.00      0.00         2\n",
      "       13.0       1.00      1.00      1.00         1\n",
      "       14.0       1.00      1.00      1.00         3\n",
      "       15.0       1.00      0.50      0.67         2\n",
      "       17.0       1.00      1.00      1.00         3\n",
      "       18.0       1.00      1.00      1.00         1\n",
      "       19.0       1.00      1.00      1.00         1\n",
      "       20.0       1.00      1.00      1.00         1\n",
      "       21.0       1.00      1.00      1.00         1\n",
      "       22.0       1.00      1.00      1.00         3\n",
      "       23.0       1.00      1.00      1.00         2\n",
      "       24.0       1.00      1.00      1.00         1\n",
      "       25.0       1.00      1.00      1.00         1\n",
      "       26.0       1.00      0.75      0.86         4\n",
      "       27.0       1.00      1.00      1.00         2\n",
      "       28.0       0.33      0.50      0.40         2\n",
      "       29.0       1.00      1.00      1.00         1\n",
      "       31.0       0.00      0.00      0.00         0\n",
      "       32.0       1.00      1.00      1.00         3\n",
      "       33.0       1.00      1.00      1.00         1\n",
      "       34.0       0.33      1.00      0.50         1\n",
      "       35.0       1.00      1.00      1.00         1\n",
      "       36.0       1.00      1.00      1.00         2\n",
      "       37.0       1.00      1.00      1.00         2\n",
      "       38.0       1.00      1.00      1.00         4\n",
      "       39.0       1.00      1.00      1.00         4\n",
      "\n",
      "avg / total       0.94      0.91      0.92        80\n",
      "\n"
     ]
    }
   ],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "from sklearn.metrics import classification_report\n",
    "\n",
    "y_pred = rand_search_cv.predict(X_test_pca)\n",
    "accuracy = accuracy_score(y_test, y_pred)\n",
    "print(\"Classifier's accuracy:\", accuracy)\n",
    "print(classification_report(y_test, y_pred))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Linear Discriminant Analysis (LDA)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "LDA reduced dimensionality from 4096 to 39\n",
      "New training and test data (320, 39) and (80, 39)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.discriminant_analysis import LinearDiscriminantAnalysis\n",
    "\n",
    "lda = LinearDiscriminantAnalysis(n_components = 118)\n",
    "\n",
    "lda.fit(X_train, y_train)\n",
    "X_train_lda = lda.transform(X_train)\n",
    "X_test_lda = lda.transform(X_test)\n",
    "\n",
    "n_comp = X_train_lda.shape[1]\n",
    "print(\"LDA reduced dimensionality from {} to {}\".format(X_train.shape[1], n_comp))\n",
    "print(\"New training and test data\", X_train_lda.shape, \"and\", X_test_lda.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fitting 3 folds for each of 10 candidates, totalling 30 fits\n",
      "Randomized search took 0.018037609259287515 minutes\n",
      "Best estimator:\n",
      "SVC(C=1.4395175817157992, cache_size=7000, class_weight=None, coef0=0.0,\n",
      "  decision_function_shape='ovr', degree=3, gamma=0.0016982001626888103,\n",
      "  kernel='rbf', max_iter=-1, probability=False, random_state=None,\n",
      "  shrinking=True, tol=0.001, verbose=False)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[Parallel(n_jobs=1)]: Done  30 out of  30 | elapsed:    1.0s finished\n"
     ]
    }
   ],
   "source": [
    "from time import time\n",
    "from sklearn.svm import SVC\n",
    "from sklearn.model_selection import RandomizedSearchCV\n",
    "from scipy.stats import reciprocal, uniform\n",
    "\n",
    "svm_clf2 = SVC(kernel = \"rbf\", cache_size = 7000)\n",
    "param_grid = {\"gamma\": reciprocal(0.001, 0.1), \"C\": uniform(1, 10)}\n",
    "rand_search_cv1 = RandomizedSearchCV(svm_clf2, param_grid, n_iter = 10, verbose = 1)\n",
    "\n",
    "start = time()\n",
    "rand_search_cv1.fit(X_train_lda, y_train)\n",
    "print(\"Randomized search took {} minutes\".format((time() - start)/60.0))\n",
    "\n",
    "print(\"Best estimator:\")\n",
    "print(rand_search_cv1.best_estimator_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Classifier's accuracy: 0.9625\n",
      "             precision    recall  f1-score   support\n",
      "\n",
      "        0.0       0.60      1.00      0.75         3\n",
      "        1.0       1.00      1.00      1.00         1\n",
      "        2.0       1.00      1.00      1.00         2\n",
      "        3.0       1.00      1.00      1.00         4\n",
      "        4.0       1.00      1.00      1.00         3\n",
      "        5.0       1.00      1.00      1.00         3\n",
      "        7.0       1.00      0.67      0.80         6\n",
      "        8.0       1.00      1.00      1.00         2\n",
      "        9.0       1.00      1.00      1.00         2\n",
      "       10.0       1.00      1.00      1.00         2\n",
      "       11.0       1.00      1.00      1.00         3\n",
      "       12.0       1.00      1.00      1.00         2\n",
      "       13.0       1.00      1.00      1.00         1\n",
      "       14.0       1.00      1.00      1.00         3\n",
      "       15.0       1.00      1.00      1.00         2\n",
      "       17.0       1.00      1.00      1.00         3\n",
      "       18.0       1.00      1.00      1.00         1\n",
      "       19.0       1.00      1.00      1.00         1\n",
      "       20.0       1.00      1.00      1.00         1\n",
      "       21.0       1.00      1.00      1.00         1\n",
      "       22.0       1.00      1.00      1.00         3\n",
      "       23.0       1.00      1.00      1.00         2\n",
      "       24.0       1.00      1.00      1.00         1\n",
      "       25.0       1.00      1.00      1.00         1\n",
      "       26.0       1.00      1.00      1.00         4\n",
      "       27.0       1.00      1.00      1.00         2\n",
      "       28.0       1.00      0.50      0.67         2\n",
      "       29.0       1.00      1.00      1.00         1\n",
      "       32.0       1.00      1.00      1.00         3\n",
      "       33.0       1.00      1.00      1.00         1\n",
      "       34.0       0.50      1.00      0.67         1\n",
      "       35.0       1.00      1.00      1.00         1\n",
      "       36.0       1.00      1.00      1.00         2\n",
      "       37.0       1.00      1.00      1.00         2\n",
      "       38.0       1.00      1.00      1.00         4\n",
      "       39.0       1.00      1.00      1.00         4\n",
      "\n",
      "avg / total       0.98      0.96      0.96        80\n",
      "\n"
     ]
    }
   ],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "from sklearn.metrics import classification_report\n",
    "\n",
    "y_pred = rand_search_cv1.predict(X_test_lda)\n",
    "accuracy = accuracy_score(y_test, y_pred)\n",
    "print(\"Classifier's accuracy:\", accuracy)\n",
    "print(classification_report(y_test, y_pred))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.3"
  },
  "widgets": {
   "state": {},
   "version": "1.1.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
